/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.sothree.slidinguppanel;

import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorValue;
import ohos.agp.components.Component;
import ohos.agp.utils.Point;
import ohos.agp.window.service.Display;
import ohos.agp.window.service.DisplayManager;
import ohos.multimodalinput.event.TouchEvent;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Locale;
import java.util.Optional;

import static com.sothree.slidinguppanel.SlidingUpPanel.State.HIDDEN;
import static com.sothree.slidinguppanel.SlidingUpPanel.State.SHOWED;

/**
 * SlidingUpPanel
 */
public class SlidingUpPanel implements Component.TouchEventListener, ValueAnimator.ValueUpdateListener,
        Animator.StateChangedListener, NotifierDetails {
    private static final String TAG = SlidingUpPanel.class.getSimpleName();

    private float mViewHeight;

    private float mViewWidth;

    private SlidingUpBuilder mBuilder;

    private SlidingUpVerticalTouchEvent mSlidingVerticalTouchEvent;

    private AnimationProcessor mAnimationProcessor;

    /**
     * constructor
     *
     * @param builder SlidingUpBuilder
     */
    SlidingUpPanel(SlidingUpBuilder builder) {
        mBuilder = builder;
        init();
    }

    private void init() {
        mBuilder.mSliderView.setTouchEventListener(this);
        if (mBuilder.mAlsoScrollView != null) {
            mBuilder.mAlsoScrollView.setTouchEventListener(this);
        }
        mBuilder.mSliderView.setLayoutRefreshedListener(component -> {
            mViewHeight = mBuilder.mSliderView.getHeight();
            mViewWidth = mBuilder.mSliderView.getWidth();
            switch (mBuilder.mStartGravity) {
                case Gravity.TOP:
                    mBuilder.mSliderView.setPivotY(mViewHeight);
                    setTouchableAreaVertical();
                    break;
                case Gravity.BOTTOM:
                    mBuilder.mSliderView.setPivotY(0);
                    setTouchableAreaVertical();
                    break;
                case Gravity.START:
                    mBuilder.mSliderView.setPivotX(0);
                    setTouchableAreaHorizontal();
                    break;
                case Gravity.END:
                    mBuilder.mSliderView.setPivotX(mViewWidth);
                    setTouchableAreaHorizontal();
                    break;
            }
        });
        createConsumers();
        updateToCurrentState();
    }

    private void setTouchableAreaHorizontal() {
        if (mBuilder.mTouchableArea == 0) {
            mBuilder.mTouchableArea = (float) Math.ceil(mViewWidth / Constants.TEN_NUM_CONST);
        }
    }

    private void setTouchableAreaVertical() {
        if (mBuilder.mTouchableArea == 0) {
            mBuilder.mTouchableArea = (float) Math.ceil(mViewHeight / Constants.TEN_NUM_CONST);
        }
    }

    private void createAnimation() {
        mAnimationProcessor = new AnimationProcessor(mBuilder,
                this, this);
    }

    private void createConsumers() {
        createAnimation();
        mSlidingVerticalTouchEvent = new SlidingUpVerticalTouchEvent(mBuilder, this, mAnimationProcessor);
    }

    private void updateToCurrentState() {
        switch (mBuilder.mStartState) {
            case HIDDEN:
                hideImmediately();
                break;
            case SHOWED:
                showImmediately();
                break;
            default:
                throw new IllegalArgumentException("support value is wrong");
        }
    }


    /**
     * show
     * <p>Show view with animation</p>
     */
    public void show() {
        show(false);
    }

    /**
     * hide
     * <p>Hide view with animation</p>
     */
    public void hide() {
        hide(true);
    }

    private void hideImmediately() {
        hide(false);
    }

    private void showImmediately() {
        show(true);
    }

    private void hide(boolean immediately) {
        mAnimationProcessor.endAnimation();
        switch (mBuilder.mStartGravity) {
            case Gravity.TOP:
                hideCard(immediately, -mViewHeight);
                break;
            case Gravity.BOTTOM:
                hideCard(immediately, mViewHeight);
                break;
            default:
                throw new IllegalArgumentException("not support value");
        }
    }

    private void hideCard(boolean immediately, float value) {
        if (immediately) {
            if (mBuilder.mSliderView.getHeight() > 0) {
                mBuilder.mSliderView.setTranslationY(value);
                notifyPercentChanged(Constants.HUNDERED_NUM_CONST);
            } else {
                mBuilder.mStartState = HIDDEN;
            }
        } else {
            Optional<Display> mOptional = DisplayManager.getInstance()
                    .getDefaultDisplay(mBuilder.mSliderView.getContext());
            Display display = mOptional.get();
            Point mDisplayPoint = new Point();
            display.getSize(mDisplayPoint);
            mAnimationProcessor.setValuesAndStart(mBuilder.mSliderView.getTranslationY(),
                    mDisplayPoint.position[Constants.ONE_NUM_CONSTANT] - Constants.FIVE_HUNDERED_NUM_CONSTANT);
        }
    }

    /**
     * hide
     * <p>notifySlidingHide view with animation</p>
     */
    public void notifySlidingHide() {
        Optional<Display> mOptional = DisplayManager.getInstance()
                .getDefaultDisplay(mBuilder.mSliderView.getContext());
        Display display = mOptional.get();
        Point mDisplayPoint = new Point();
        display.getSize(mDisplayPoint);
        mAnimationProcessor.setValuesAndStart(mBuilder.mSliderView.getTranslationY(),
                mDisplayPoint.position[Constants.ONE_NUM_CONSTANT] - Constants.FIVE_HUNDERED_NUM_CONSTANT);
    }

    private void show(boolean immediately) {
        mAnimationProcessor.endAnimation();
        switch (mBuilder.mStartGravity) {
            case Gravity.TOP:
                showCard(immediately);
                break;
            case Gravity.BOTTOM:
                showCard(immediately);
                break;
            default:
                throw new IllegalArgumentException("not support value");
        }
    }

    private void showCard(boolean immediately) {
        if (immediately) {
            if (mBuilder.mSliderView.getHeight() > 0) {
                mBuilder.mSliderView.setTranslationY(0);
                notifyPercentChanged(0);
            } else {
                mBuilder.mStartState = SHOWED;
            }
        } else {
            float translationY = mBuilder.mSliderView.getTranslationY();
            mAnimationProcessor.setValuesAndStart(mBuilder.mSliderView.getTranslationY(), 0);
        }
    }

    /**
     * On touch event boolean
     *
     * @param component  Component
     * @param touchEvent TouchEvent
     * @return the boolean
     */
    @Override
    public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
        if (component == mBuilder.mSliderView) {
            LogUtil.info(TAG, " onTouchEvent mSliderView " + touchEvent.getAction());
        }

        if (component == mBuilder.mAlsoScrollView) {
            LogUtil.info(TAG, " onTouchEvent mAlsoScrollView");
        }
        if (mAnimationProcessor.isAnimationRunning()) {
            return false;
        }

        if (!mBuilder.isGesturesEnabled) {
            mBuilder.mSliderView.simulateClick();
            return true;
        }
        boolean consumed;
        switch (mBuilder.mStartGravity) {
            case Gravity.TOP:
                consumed = mSlidingVerticalTouchEvent.consumeTopToBottom(component, touchEvent);
                break;
            case Gravity.BOTTOM:
                consumed = mSlidingVerticalTouchEvent.consumeBottomToTop(component, touchEvent);
                break;
            default:
                throw new IllegalArgumentException("You are using not supported gravity");
        }
        if (!consumed) {
            mBuilder.mSliderView.simulateClick();
        }
        return true;
    }

    /**
     * onUpdate
     *
     * @param animatorValue AnimatorValue
     * @param value         float
     */
    @Override
    public void onUpdate(AnimatorValue animatorValue, float value) {
        switch (mBuilder.mStartGravity) {
            case Gravity.TOP:
                onAnimationUpdateTopToBottom(value);
                break;
            case Gravity.BOTTOM:
                onAnimationUpdateBottomToTop(value);
                break;
            default:
                throw new IllegalArgumentException("You are using not supported value");
        }
    }

    private void onAnimationUpdateTopToBottom(float value) {
        mBuilder.mSliderView.setTranslationY(value);
        float visibleDistance = mBuilder.mSliderView.getTop() - mBuilder.mSliderView.getContentPositionY();
        float percents = visibleDistance * Constants.HUNDERED_NUM_CONST / mViewHeight;
        notifyPercentChanged(percents);
    }

    private void onAnimationUpdateBottomToTop(float value) {
        mBuilder.mSliderView.setTranslationY(value);
        float visibleDistance = mBuilder.mSliderView.getContentPositionY() - mBuilder.mSliderView.getTop();
        float percents = visibleDistance * Constants.HUNDERED_NUM_CONST / mViewHeight;
        notifyPercentChanged(percents);
    }


    /**
     * Notify percent changed *
     *
     * @param percent float
     */
    @Override
    public void notifyPercentChanged(float percent) {
        float perc = percent;
        perc = perc > Constants.HUNDERED_NUM_CONST ? Constants.HUNDERED_NUM_CONST : perc;
        perc = perc < 0 ? 0 : perc;
        if (perc == Constants.HUNDERED_NUM_CONST) {
            mBuilder.mSliderView.setVisibility(Component.HIDE);
            notifyVisibilityChanged(Component.HIDE);
        } else {
            mBuilder.mSliderView.setVisibility(Component.VISIBLE);
            if (perc == 0) {
                notifyVisibilityChanged(Component.VISIBLE);
            }
        }
        if (!mBuilder.mListeners.isEmpty()) {
            for (int i = 0; i < mBuilder.mListeners.size(); i++) {
                Listener listener = mBuilder.mListeners.get(i);
                if (listener != null) {
                    if (listener instanceof Listener.Slide) {
                        Listener.Slide slide = (Listener.Slide) listener;
                        slide.onSlide(perc);
                    }
                } else {
                    logError(i, "onSlide");
                }
            }
        }
    }

    /**
     * Notify visibility changed *
     *
     * @param visibility int
     */
    @Override
    public void notifyVisibilityChanged(int visibility) {
        if (!mBuilder.mListeners.isEmpty()) {
            for (int i = 0; i < mBuilder.mListeners.size(); i++) {
                Listener listener = mBuilder.mListeners.get(i);
                if (listener != null) {
                    if (listener instanceof Listener.Visibility) {
                        Listener.Visibility vis = (Listener.Visibility) listener;
                        vis.onVisibilityChanged(visibility);
                    }
                } else {
                    logError(i, "onVisibilityChanged");
                }
            }
        }
        switch (visibility) {
            case Component.VISIBLE:
                State mCurrentState = SHOWED;
                break;
            case Component.HIDE:
                mCurrentState = HIDDEN;
                break;
            default:
                throw new IllegalArgumentException("You are using not supported value");
        }
    }

    /**
     * On start *
     *
     * @param animator animator
     */
    @Override
    public void onStart(Animator animator) {
    }

    /**
     * On stop *
     *
     * @param animator animator
     */
    @Override
    public void onStop(Animator animator) {
        // Do something
    }

    /**
     * On cancel *
     *
     * @param animator animator
     */
    @Override
    public void onCancel(Animator animator) {
        // Do something
    }

    /**
     * On end *
     *
     * @param animator animator
     */
    @Override
    public void onEnd(Animator animator) {
        // Do something
    }

    /**
     * On pause *
     *
     * @param animator animator
     */
    @Override
    public void onPause(Animator animator) {
        // Do something
    }

    /**
     * On resume *
     *
     * @param animator animator
     */
    @Override
    public void onResume(Animator animator) {
        // Do something
    }

    /**
     * Log error *
     *
     * @param listener listener
     * @param method   method
     */
    private void logError(int listener, String method) {
        if (mBuilder.mDebug) {
            LogUtil.info(TAG, String.format(Locale.ROOT, "Listener(%1s) (%2$-23s)" +
                    " Listener is null, skip notification...", listener, method));
        }
    }

    /**
     * <p>Available start states</p>
     * State enum
     */
    public enum State {
        /**
         * State hidden is equal
         */
        HIDDEN,
        /**
         * State showed is equal
         */
        SHOWED
    }

    /**
     * Start vector@IntDef(value = {Gravity.START, Gravity.END, Gravity.TOP, Gravity.BOTTOM})
     */
    @Retention(RetentionPolicy.SOURCE)
    @interface StartVector {
    }

    /**
     * Slide
     */
    public interface Listener {
        interface Slide extends Listener {
            void onSlide(float percent);
        }

        interface Visibility extends Listener {
            void onVisibilityChanged(int visibility);
        }

        interface Events extends Visibility, Slide {
        }
    }
}
